/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package studio.raptor.ddal.config.reloading;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Sam
 * @since 3.0.0
 */
public class PeriodicReloadingTrigger {

  /**
   * The executor service used by this trigger.
   */
  private final ScheduledExecutorService executorService;

  /**
   * The associated reloading controller.
   */
  private final ReloadingController controller;

  /**
   * The parameter to be passed to the controller.
   */
  private final Object controllerParam;

  /**
   * The fixedDelay.
   */
  private final long fixedDelay;

  /**
   * The time unit.
   */
  private final TimeUnit timeUnit;

  /**
   * Stores the future object for the current trigger task.
   */
  private ScheduledFuture<?> triggerTask;


  /**
   * Creates a new instance of {@code PeriodicReloadingTrigger} and sets all
   * parameters.
   *
   * @param ctrl the {@code ReloadingController} (must not be <b>null</b>)
   * @param ctrlParam the optional parameter to be passed to the controller when doing reloading
   * checks
   * @param fixedDelay the period in which the controller is triggered
   * @param unit the time unit for the period
   * @param exec the executor service to use (can be <b>null</b>, then a default executor service is
   * created
   * @throws IllegalArgumentException if a required argument is missing
   */
  public PeriodicReloadingTrigger(ReloadingController ctrl, Object ctrlParam,
      long fixedDelay, TimeUnit unit, ScheduledExecutorService exec) {
    if (ctrl == null) {
      throw new IllegalArgumentException("ReloadingController must not be null!");
    }
    controller = ctrl;
    controllerParam = ctrlParam;
    this.fixedDelay = fixedDelay;
    timeUnit = unit;
    executorService = (exec != null) ? exec : createDefaultExecutorService();
  }

  /**
   * Creates a new instance of {@code PeriodicReloadingTrigger} with a default
   * executor service.
   *
   * @param ctrl the {@code ReloadingController} (must not be <b>null</b>)
   * @param ctrlParam the optional parameter to be passed to the controller when doing reloading
   * checks
   * @param triggerPeriod the period in which the controller is triggered
   * @param unit the time unit for the period
   * @throws IllegalArgumentException if a required argument is missing
   */
  public PeriodicReloadingTrigger(ReloadingController ctrl, Object ctrlParam,
      long triggerPeriod, TimeUnit unit) {
    this(ctrl, ctrlParam, triggerPeriod, unit, null);
  }

  /**
   * Starts this trigger. The associated {@code ReloadingController} will be
   * triggered according to the specified period. The first triggering happens
   * after a period. If this trigger is already started, this invocation has
   * no effect.
   */
  public synchronized void start() {
    if (!isRunning()) {
//      triggerTask =
//          getExecutorService().scheduleAtFixedRate(
//              createTriggerTaskCommand(), period, period,
//              timeUnit);

      triggerTask =
          getExecutorService().scheduleWithFixedDelay(
              createTriggerTaskCommand(), fixedDelay, fixedDelay,
              timeUnit);
    }
  }


  /**
   * Stops this trigger. The associated {@code ReloadingController} is no more
   * triggered. If this trigger is already stopped, this invocation has no
   * effect.
   */
  public synchronized void stop() {
    if (isRunning()) {
      triggerTask.cancel(false);
      triggerTask = null;
    }
  }

  /**
   * Returns a flag whether this trigger is currently active.
   *
   * @return a flag whether this trigger is running
   */
  public synchronized boolean isRunning() {
    return triggerTask != null;
  }

  /**
   * Shuts down this trigger and optionally shuts down the
   * {@code ScheduledExecutorService} used by this object. This method should
   * be called if this trigger is no more needed. It ensures that the trigger
   * is stopped. If the parameter is <b>true</b>, the executor service is also
   * shut down. This should be done if this trigger is the only user of this
   * executor service.
   *
   * @param shutdownExecutor a flag whether the associated {@code ScheduledExecutorService} is to be
   * shut down
   */
  public void shutdown(boolean shutdownExecutor) {
    stop();
    if (shutdownExecutor) {
      getExecutorService().shutdown();
    }
  }

  /**
   * Shuts down this trigger and its {@code ScheduledExecutorService}. This is
   * a shortcut for {@code shutdown(true)}.
   *
   * @see #shutdown(boolean)
   */
  public void shutdown() {
    shutdown(true);
  }

  /**
   * Returns the {@code ScheduledExecutorService} used by this object.
   *
   * @return the associated {@code ScheduledExecutorService}
   */
  ScheduledExecutorService getExecutorService() {
    return executorService;
  }

  /**
   * Creates the task which triggers the reloading controller.
   *
   * @return the newly created trigger task
   */
  private Runnable createTriggerTaskCommand() {
    return new Runnable() {
      @Override
      public void run() {
        controller.checkForReloading(controllerParam);
      }
    };
  }

  /**
   * Creates a default executor service. This method is called if no executor
   * has been passed to the constructor.
   *
   * @return the default executor service
   */
  private static ScheduledExecutorService createDefaultExecutorService() {
    return Executors
        .newScheduledThreadPool(1, new ReloadingNamingThreadFactory("ReloadingTrigger-%s"));
  }

  private static class ReloadingNamingThreadFactory implements ThreadFactory {

    private final AtomicLong threadCounter;
    private final String namingPattern;

    private ReloadingNamingThreadFactory(String namingPattern) {
      this.namingPattern = namingPattern;
      this.threadCounter = new AtomicLong(0);
    }

    @Override
    public Thread newThread(Runnable r) {

      Thread thread = new Thread(r);
      thread.setDaemon(true);

      if (this.namingPattern != null) {
        final Long count = threadCounter.incrementAndGet();
        thread.setName(String.format(this.namingPattern, count));
      }
      return thread;
    }
  }
}
